﻿/* Copyright (C) Siemens AG, 2012. All Rights Reserved.
 * Author: Roland Eckl, Corporate Technology
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Windows.Input;
using RVM.Infrastructure.Communication;


namespace RVM.Infrastructure
{
    /// <summary>
    /// Proxy that can be connected to existing (non-remotable) ViewModels based on INotifyPropertyChanged.
    /// </summary>
    public class ViewModelProxy : RemotableViewModel
    {
        #region Private Members

        /// <summary>
        /// Private member of the associated viewmodel this class is the proxy for,
        /// </summary>
        private INotifyPropertyChanged _associatedViewModel = null;

        /// <summary>
        /// Flag wether to surpress PropertyChanged events.
        /// </summary>
        private bool _surpressNotification = false;

        /// <summary>
        /// Flag that prevents reentry on PropertyChanged event cycles.
        /// </summary>
        private bool _surpressReentry = false;

        /// <summary>
        /// Lock object to avoid concurrency when (de-)registering for collection events.
        /// </summary>
        private object _collectionLock = new object();

        #endregion

        #region Public Members

        /// <summary>
        /// Public (read-only) accessor for the associated ViewModel.
        /// </summary>
        public INotifyPropertyChanged Associated
        {
            get { return _associatedViewModel; }
        }

        #endregion

        #region Constrcutors

        /// <summary>
        /// Default constructor with all parameters.
        /// </summary>
        /// <param name="associated">Associated ViewModel (traditional)</param>
        /// <param name="id"></param>
        /// <param name="registry"></param>
        public ViewModelProxy(INotifyPropertyChanged associated, string id, ViewModelRegistry registry)
        {
            if(associated == null)
                throw new ArgumentNullException("No associated ViewModel given.");
            if(associated.GetType().IsAssignableFrom(typeof(RemotableViewModel)))
                throw new ArgumentException("There should be no proxy for a RemotableViewModel. Work with viewmodel directly!");

            // Connect to PropertyChanged event
            _associatedViewModel = associated;
            _associatedViewModel.PropertyChanged += OnAssociatedViewModelPropertyChanged;

            if (id != null)
                ExportViewModel(id, registry ?? ViewModelRegistry.Shared);

            // Option: Create properties according source per reflection? (Could be used for some transparent CreateViewModel without implemented class signature?!)
        }

        /// <summary>
        /// Constructor that exports the associated viewmodel to a specific registry.
        /// </summary>
        /// <param name="associated"></param>
        /// <param name="id"></param>
        public ViewModelProxy(INotifyPropertyChanged associated, string id) : this(associated, id, null) { }

        /// <summary>
        /// Constructor that does not export the viewmodel at the moment.
        /// </summary>
        /// <param name="associated"></param>
        public ViewModelProxy(INotifyPropertyChanged associated) : this(associated, null, null) { }

        #endregion

        #region Overriden Methods

        /// <summary>
        /// Injects proxy and callback channel into this ViewModel to wire up the connection to the exporting/remote ViewModel.
        /// </summary>
        /// <param name="id">Unique ID of the ViewModel that is imported</param>
        /// <param name="callback">Callback channel</param>
        /// <param name="agent">Proxy</param>
        internal override void InjectRemoteViewModelAgent(string id, IViewModelCallback callback, IViewModelAgent agent, IViewModelImporter importer)
        {
            base.InjectRemoteViewModelAgent(id, callback, agent, importer);
            // Initially the viewmodel must be synchronized with remote viewmodel.
            Synchronize();
        }

        /// <summary>
        /// Event handler, is raised whenever the exporting/remote ViewModel sends a
        /// property changed event through the callback channel.
        /// </summary>
        /// <param name="id"></param>
        /// <param name="property"></param>
        internal override void OnRemotePropertyChanged(string id, string property)
        {
            // Only fire events that are targeting this actual viewmodel
            if (_identifier != null && _identifier.Equals(id) && !_surpressReentry)
            {
                _surpressReentry = true;
                object value = GetValue(property);
                SetAssociatedValue(property, value);
                _surpressReentry = false;
            }
        }

        /// <summary>
        /// Gets the value for a specific property.
        /// If the ViewModel is from the application's point of view a local one,
        /// the value is taken from the _properties dictionary. If it is a remote one,
        /// the proxy towards the remote ViewModel is called.
        /// </summary>
        /// <param name="propertyName">Property of interest</param>
        /// <param name="defaultValue">Value if there is no content (yet)</param>
        /// <returns>Current value of the property</returns>
        public override object GetValue(string propertyName, object defaultValue = null)
        {
            // If there is a remote object, get it's value
            // TODO: Maybe simply return the associated viewmodel's value? Values should be synchronized anyhow.
            if (_localProxy != null)
            {
                try
                {
                    // TODO: Implement get-logic for nested viewmodels according to base class! Can this be moved to a protected block shared for both, Proxy and RVM? (the _localProxy!=null block seems to be identical)
                    return _localProxy.GetValue(_identifier, propertyName, defaultValue);
                }
                catch(Exception ex)
                {
                    // Seems the proxy is corrupt,
                    RemoveRemoteViewModelAgent();
                }
            }

            return GetAssociatedValue(propertyName, null);
        }

        /// <summary>
        /// Sets the value for a specific property.
        /// If the ViewModel is from the application's point of view a local one,
        /// the value is put into the _properties dictionary. If it is a remote one,
        /// the proxy towards the remote ViewModel is called.
        /// </summary>
        /// <param name="propertyName">Property that changed</param>
        /// <param name="value">New value</param>
        /// <param name="supressNotification">Fire PropertyChanged event for this set-operation?</param>
        public override void SetValue(string propertyName, object value, bool supressNotification = false)
        {
            // Set flag for notification
            _surpressNotification = supressNotification;

            SetRemoteValue(propertyName, value);
            // To keep in sync, set the associated viewmodel's value also!
            SetAssociatedValue(propertyName, value);

            // Remove flag for notification
            _surpressNotification = false;
        }

        /// <summary>
        /// Calls Execute on a command either locally or on an exported remote partner.
        /// </summary>
        /// <param name="commandName">Command's name</param>
        /// <param name="parameter">Parameter to hand over</param>
        public override void ExecuteCommand(string commandName, object parameter)
        {
            // TODO: This one may be tricky if the command should have local effect. (Could add attributes later, e.g. [Export(false)] and then take local command always!)
            // If there is an exporting/remote object, set it's value
            if (_localProxy != null)
            {
                try
                {
                    _localProxy.ExecuteCommand(_identifier, commandName, parameter);
                }
                catch (Exception ex)
                {
                    // Seems the proxy is corrupt,
                    RemoveRemoteViewModelAgent();
                }
            }
            else
            {
                ICommand command = GetCommand(commandName);
                if (command != null)
                    command.Execute(parameter);
            }
        }

        /// <summary>
        /// Calls CanExecute on a command either locally or on an exported remote partner.
        /// </summary>
        /// <param name="commandName">Command's name</param>
        /// <param name="parameter">Parameter to hand over</param>
        /// <returns>TRUE, if condition is met and command can be executed</returns>
        public override bool CanExecuteCommand(string commandName, object parameter)
        {
            // If there is an exporting/remote object, set it's value
            if (_localProxy != null)
            {
                try
                {
                    return _localProxy.CanExecuteCommand(_identifier, commandName, parameter);
                }
                catch (Exception ex)
                {
                    // Seems the proxy is corrupt,
                    RemoveRemoteViewModelAgent();
                }
            } 

            ICommand command = GetCommand(commandName);
            if (command != null)
                return command.CanExecute(parameter);

            return false;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Helper method, gets an ICommand object from the sourve ViewModel.
        /// </summary>
        /// <param name="commandName">Property name of the requested command</param>
        /// <returns>ICommand object or null</returns>
        private ICommand GetCommand(string commandName)
        {
            if (_associatedViewModel == null)
                return null;
            PropertyInfo propertyInfo = _associatedViewModel.GetType().GetProperty(commandName);
            return propertyInfo.GetValue(_associatedViewModel, null) as ICommand;
        }

        /// <summary>
        /// Event Handler that gets raised whenever the associated viewmodels PropertyChanged event is fired.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnAssociatedViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (Location == ViewModelLocation.Local)
            {
                if (IsNestedViewModel(e.PropertyName))
                {
                    // TODO: Shares a lot of code with SetValue from base class.
                    if (!_associatedRegistry.Contains(_identifier + "." + e.PropertyName))
                    {
                        object nested = GetAssociatedValue(e.PropertyName, null);
                        RemotableViewModel rvm = nested as RemotableViewModel;
                        if (rvm != null)
                            // Is explicitely a RemoteViewModel, not only implementing INotifyPropertyChanged.
                            rvm.ExportViewModel(_identifier + "." + e.PropertyName, _associatedRegistry);
                        else // Implements at least INotifyPropertyChanged.
                            new ViewModelProxy(nested as INotifyPropertyChanged).ExportViewModel(
                                _identifier + "." + e.PropertyName, _associatedRegistry);
                    }
                }

                // If was set through proxy, do not fire again.
                if (!_surpressNotification)
                    FirePropertyChanged(e.PropertyName);
            }
            else
                SetRemoteValue(e.PropertyName, GetAssociatedValue(e.PropertyName, null));
        }

        /// <summary>
        /// Gets the type of a property from the associated viewmodel.
        /// </summary>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        private Type GetAssociatedType(string propertyName)
        {
            if (_associatedViewModel == null)
                return null;
            PropertyInfo propertyInfo = _associatedViewModel.GetType().GetProperty(propertyName);
            return propertyInfo.PropertyType;
        }

        /// <summary>
        /// Gets the value of a property from the associated viewmodel.
        /// </summary>
        /// <param name="propertyName"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        private object GetAssociatedValue(string propertyName, object defaultValue)
        {
            if (_associatedViewModel == null)
                return defaultValue;
            
            PropertyInfo propertyInfo = _associatedViewModel.GetType().GetProperty(propertyName);
            object returnValue = propertyInfo.GetValue(_associatedViewModel, null);

            // TODO: This looks quite familiar (RVM.GetValue); move to shared method for proxy and rvm?
            //if (returnValue is INotifyCollectionChanged)
            //{
            //    var x = returnValue as IList;

            //    object[] m = new object[x.Count];
            //    for (int i = 0; i < x.Count; i++)
            //    {
            //        m[i] = x[i];
            //    }

            //    return m;
            //}

            return returnValue;
        }

        /// <summary>
        /// Sets the value for a property on the remote partner's side.
        /// </summary>
        /// <param name="propertyName"></param>
        /// <param name="value"></param>
        /// <param name="surpressNotification"></param>
        private void SetRemoteValue(string propertyName, object value, bool surpressNotification = false)
        {
            // If there is an exporting/remote object, set it's value
            if (_localProxy != null)
            {
                try
                {
                    _localProxy.SetValue(_identifier, propertyName, value, surpressNotification);
                }
                catch
                {
                    // Seems the proxy is corrupt,
                    RemoveRemoteViewModelAgent();
                }
            }
        }

        /// <summary>
        /// Sets the value of a property from the associated viewmodel.
        /// </summary>
        /// <param name="propertyName"></param>
        /// <param name="value"></param>
        private void SetAssociatedValue(string propertyName, object value)
        {
            // To keep in sync, set the associated viewmodel's value also!
            if(_localProxy != null) // Remote side
                _associatedViewModel.PropertyChanged -= OnAssociatedViewModelPropertyChanged;

            if (_associatedViewModel == null)
                return;
            PropertyInfo propertyInfo = _associatedViewModel.GetType().GetProperty(propertyName);
            propertyInfo.SetValue(_associatedViewModel, value, null);

            if(_localProxy != null) // Remote side
                _associatedViewModel.PropertyChanged += OnAssociatedViewModelPropertyChanged;
        }

        /// <summary>
        /// Synchronize with remote ViewModel. Every property of the associated "traditional" ViewModel is requested.
        /// </summary>
        private void Synchronize()
        {
            if(Location == ViewModelLocation.Remote)
            {
                PropertyInfo[] properties = _associatedViewModel.GetType().GetProperties();
                foreach (PropertyInfo p in properties)
                {
                    if(p.PropertyType.IsAssignableFrom(typeof(ICommand)))
                    {
                        ////////////
                        // Commands

                        if (!p.CanWrite)
                            throw new AccessViolationException("Commands must not be read-only."); // TODO: Ignore incompatible commands/types? Make this behavior configurable?

                        // Redirect commands to the remote side!
                        string name = p.Name; // TODO: Best practice to fill value into anonymous method? If not creating local variable, p.Name would have another value when p changes!
                        RemotableCommand redirection = new RemotableCommand(p.Name, param => ExecuteCommand(name, param),
                                                                                      param => CanExecuteCommand(name, param));
                        SetAssociatedValue(p.Name, redirection);
                        ////////////
                    }
                    else if(IsNestedViewModel(p.Name))
                    {
                        ////////////
                        // Nested ViewModels

                        // TODO: Test me ;-)  ... seems Import works. But more likely never exported yet!
                        if (_importer != null) // For local case the viewmodel can simply be taken from the dictionary.
                        {
                            // Try to import...
                            object vm = _importer.GetViewModel(p.PropertyType, _identifier + "." + p.Name);
                            if (vm != null)
                            {
                                if (vm is ViewModelProxy)
                                    // traditional viewmodels have a proxy, return associated model instead.
                                    SetAssociatedValue(p.Name, ((ViewModelProxy) vm).Associated);
                                else
                                    SetAssociatedValue(p.Name, vm);
                            }
                        }

                        ////////////
                    }
                    else if(p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(ObservableCollection<>))
                    {
                        ////////////
                        // Collections

                        _surpressReentry = true;
                        INotifyCollectionChanged collection = CreateGeneric(typeof(ObservableCollection<>), p.PropertyType.GetGenericArguments()[0]) as INotifyCollectionChanged;
                        collection.CollectionChanged += OnLocalCollectionChanged;

                        var content = _localProxy.GetCollection(_identifier, p.Name);
                        if (content != null)
                        {
                            foreach (object item in content)
                            {
                                ((IList)collection).Add(item);
                            }
                        }
                        SetAssociatedValue(p.Name, collection);
                        _surpressReentry = false;

                        ////////////
                    }
                    else if (p.PropertyType.IsSerializable)
                    {
                        ////////////
                        // Properties

                        // Only work with properties that are serializable.
                        // Any other will possibly corrupt the transport channel.
                        _surpressReentry = true;
                        object value = GetValue(p.Name);
                        SetAssociatedValue(p.Name, value);
                        _surpressReentry = false;

                        ////////////
                    }
                }
            }
        }

        /// <summary>
        /// Gets a generic collection by name.
        /// </summary>
        /// <param name="collectionName"></param>
        /// <returns></returns>
        internal override IList GetCollection(string collectionName)
        {
            object val = GetAssociatedValue(collectionName, null);
            if(val is INotifyCollectionChanged)
            {
                lock (_collectionLock)
                {
                    // Ensure we have subscribed for CollectionChanged event (only once)
                    // TODO: Lock access to event! Could be concurrency problem with multiple importers!
                    ((INotifyCollectionChanged) val).CollectionChanged -= OnLocalCollectionChanged;
                    ((INotifyCollectionChanged) val).CollectionChanged += OnLocalCollectionChanged;
                }
            }
            return val as IList;
        }

        public static object CreateGeneric(Type generic, Type innerType, params object[] args)
        {
            Type specificType = generic.MakeGenericType(new [] { innerType });
            return Activator.CreateInstance(specificType, args);
        }

        /// <summary>
        /// Event handler, is raised whenever an exporting/remote ViewModel's collection sends a
        /// collection changed event locally.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        internal override void OnLocalCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            // Local subscribers should get the CollectionChanged event themselves, nothing to do!
            if (_importingAgents.Count <= 0)
                return;
            
            // Get all properties (there could be several properties pointing to actually the same collection)
            PropertyInfo[] properties = _associatedViewModel.GetType().GetProperties();
            IEnumerable<string> affected = from property in properties where Equals(GetAssociatedValue(property.Name, null), sender) select property.Name;
            
            // For remote subscribers...
            foreach (string collection in affected)
            {
                // For remote subscribers
                for (int i = _importingAgents.Count - 1; i >= 0; i--)
                {
                    try
                    {
                        CollectionChangedEventArgs details = new CollectionChangedEventArgs();
                        details.Action = e.Action;
                        details.NewItems = e.NewItems; //(e.Action == NotifyCollectionChangedAction.Add) ? e.NewItems : e.OldItems;
                        details.NewIndex = e.NewStartingIndex;
                        details.OldItems = e.OldItems;
                        details.OldIndex = e.OldStartingIndex;
                        _importingAgents[i].FireCollectionChanged(_identifier, collection, details);
                    }
                    catch (Exception)
                    {
                        // Subscriber possible got lost...
                        RemoveViewModelAgent(_importingAgents[i]);
                    }
                }
            }
        }

        #endregion
    }
}
